---
title: Email
description: Send and receive emails with Cloudflare Email
---

RedwoodSDK integrates with [Cloudflare Email Workers](https://developers.cloudflare.com/email-routing/email-workers/) so your application can send transactional messages, receive inbound mail, and reply in the same Worker runtime.

Production deliveries currently require recipients to be verified through Cloudflare Email Routing, but Cloudflare’s forthcoming [Email Service beta](https://blog.cloudflare.com/email-service/) expands reach to general addresses.

This guide walks through the configuration steps, highlights important sending considerations, and demonstrates common patterns for end-to-end email workflows.

## Implementing Email Handling

Update your `wrangler.jsonc` to include the `EMAIL` binding:

```jsonc title="wrangler.jsonc"
{
  "send_email": [
    {
      "name": "EMAIL",
    },
  ],
}
```

Next, run `pnpm generate` to update the generated type definitions.

Once you have a zone with Email Routing enabled, follow the [Enable Email Workers](https://developers.cloudflare.com/email-routing/email-workers/enable-email-workers/) documentation to deploy your Worker in production.

Outbound email **must target a destination address that you have verified** in [Email Routing](https://developers.cloudflare.com/email-routing/email-workers/enable-email-workers/).
When calling `env.EMAIL.send()`, pass either a verified address or leave the recipient undefined when you use a binding that specifies `destination_address` or `allowed_destination_addresses`.

> ℹ️ For broader transactional delivery to arbitrary recipients, see [Cloudflare's Email Service beta](https://blog.cloudflare.com/email-service/) or an external provider such as [Resend](/guides/email/sending-email).

### Example Worker with Email Handling

The example below demonstrates how to integrate the worker with email sending and receiving.

To send and email, you can
simply call the `env.EMAIL.send()` method with the email message as
shown in the example below `route("/email", async () => { ... })`.

> ℹ️ Note: In production, this must be a verified address in [Email Routing](https://developers.cloudflare.com/email-routing/email-workers/enable-email-workers/).

However, to receive an email, you need to implement the `email` handler and
export it in the default export of the worker.

This is a change to how `defineApp` is often used in the worker.

The default export of the worker is the `DefaultWorker` class that extends the `WorkerEntrypoint` class.

This tells Cloudflare that the worker is also email worker and can be selected to route inbound emails to.

> Note: If you are just sending emails, you can still use `defineApp` as usual and just call `env.EMAIL.send()` in the route handler or in a server function.

```file title="worker.ts"
import * as PostalMime from "postal-mime";
import { EmailMessage } from "cloudflare:email";
import { createMimeMessage } from "mimetext";
import { render, route } from "rwsdk/router";
import { defineApp } from "rwsdk/worker";
import { Document } from "@/app/Document";
import { setCommonHeaders } from "@/app/headers";
import { env, WorkerEntrypoint } from "cloudflare:workers";

const app = defineApp([
  setCommonHeaders(),

  /**
   * This route is used to send an email from the worker
   * First, we create a MIME message with the sender, recipient,
   * and the content of the email.
   * Then, we create a new EmailMessage object with the
   * sender, recipient, and the raw content of the email.
   * Finally, we send the email using the `env.EMAIL.send()` method.
   * Ensure the `recipient@example.com` address is verified in Cloudflare Email Routing, or adjust the binding configuration accordingly.
   */
  route("/email", async () => {
    const msg = createMimeMessage();
    msg.setSender({ name: "Sending email test", addr: "sender@example.com" });
    msg.setRecipient("recipient@example.com");
    msg.setSubject("An email generated in a worker");
    msg.addMessage({
      contentType: "text/plain",
      data: `Congratulations, you just sent an email from a worker.`,
    });

    const message = new EmailMessage(
      "sender@example.com",
      "recipient@example.com",
      msg.asRaw()
    );
    await env.EMAIL.send(message);
    return Response.json({ ok: true });
  }),
]);

/**
 * This is the default worker entrypoint for the Worker.
 * It extends the WorkerEntrypoint class and implements the email and fetch handlers.
 */
// It extends the WorkerEntrypoint class and implements the email and fetch handlers.
export default class DefaultWorker extends WorkerEntrypoint<Env> {
  /**
   * Email handler for the Worker.
   * The `message` parameter is an ForwardableEmailMessage object
   *
   * You can call `message.reply()` to respond directly to the
   * inbound sender without additional verification steps.
   */
  async email(message: ForwardableEmailMessage) {

    const parser = new PostalMime.default();
    const rawEmail = new Response((message as any).raw);
    const email = await parser.parse(await rawEmail.arrayBuffer());
    console.log(email);
  }

  /**
   * Fetch handler for the Worker.
   * Needed so that the worker can handle the request and pass it to the app.
   */
  override async fetch(request: Request) {
    return await app.fetch(request, this.env, this.ctx);
  }
}
```

### Replying to inbound email

You can reply directly to an inbound message without pre-verifying the recipient.

The Worker runtime preserves threading headers and delivers the response through the original route. Construct your response with `mimetext` and pass the raw payload to `message.reply()`, as shown in the [Reply from Workers guide](https://developers.cloudflare.com/email-routing/email-workers/reply-email-workers/).

It is important to note that the `In-Reply-To` header is required to reply to the inbound email.

```ts title="Replying inside the email handler"
async email(message: ForwardableEmailMessage) {
  console.log("📧 Email received");

  // Parse the inbound email
  const parser = new PostalMime.default();
  const rawEmail = new Response((message as any).raw);

  const receivedEmail = await parser.parse(await rawEmail.arrayBuffer());
  console.log("📧 Email received and parsed", receivedEmail);

  // Create a new message to reply to the inbound email
  const replyToMessage = createMimeMessage();

  // ❗️ Important:This In-Reply-To header is required to reply to the inbound email
  replyToMessage.setHeader(
    "In-Reply-To",
    message.headers.get("Message-ID") ?? ""
  );

  replyToMessage.setSender({ name: "Contact Person", addr: "<SENDER>@example.com" });
  replyToMessage.setRecipient(receivedEmail.from);
  replyToMessage.setSubject(`Re: ${receivedEmail.subject}`);
  replyToMessage.addMessage({
    contentType: "text/plain",
    data: "Thanks for contacting us. We'll get back to you shortly.",
  });

  console.log("📧 New message created", replyToMessage.asRaw());

  const replyMessage = new EmailMessage(
    "<SENDER>@example.com",
    message.from,
    replyToMessage.asRaw()
  );

  console.log("📧 Sending reply email");

  await message.reply(replyMessage);

  console.log("📧 Reply email sent");
}
```

### Key points

- By default, the worker is not an email worker. You need to extend the `WorkerEntrypoint` class and implement the `email` handler to make it an email worker.
- By extending the `WorkerEntrypoint` class, you are telling Cloudflare that the worker is also email worker and can be selected to route inbound emails to.
- The `message` parameter is an `ForwardableEmailMessage` object that contains the inbound email message.
- The `In-Reply-To` header is required to reply to the inbound email.
- Use `PostalMime` to parse inbound messages for headers, text, HTML, and attachments.
- Construct outbound MIME content with `mimetext` to control subject, sender, and body.
- Call `env.EMAIL.send()` to deliver new messages, or `message.reply()` / `message.forward()` inside the email handler.
- A fetch handler is needed so that the worker can handle all requests and pass it to the app.

## Testing Locally

RedwoodSDK can [emulate](https://developers.cloudflare.com/email-routing/email-workers/local-development/) both inbound and outbound email interactions locally.

### Sending Email Locally

To test the sending of an email by the email handler locally, you can use the following command:

```bash
pnpm dev
```

This will start the local development server and you can send emails to the `recipient@example.com` address.

Now, visit `http://localhost:5173/email` to see the email in the console output of the local development server.

For this example, you'll see the following response:

```json title="Response"
{
  "ok": true
}
```

and in the console output, you'll see something like the following log:

```bash
send_email binding called with the following message:
  /var/folders/ft/8320mthj6gbdd2pmc42x13480000gn/T/miniflare-288e7109e15f898bd9877d7857386f8b/files/email/2dad29db-0a7d-498d-89ab-e961746835c4.eml
```

You can also see the email in the `.eml` file in the temporary directory.

```bash
cat /var/folders/ft/8320mthj6gbdd2pmc42x13480000gn/T/miniflare-288e7109e15f898bd9877d7857386f8b/files/email/2dad29db-0a7d-498d-89ab-e961746835c4.eml

288e7109e15f898bd9877d7857386f8b/files/email/2dad29db-0a7d-498d-89ab-e961746835c4.eml

Date: Sun, 09 Nov 2025 01:54:06 +0000
From: =?utf-8?B?U2VuZGluZyBlbWFpbCB0ZXN0?= <sender@example.com>
To: <recipient@example.com>
Message-ID: <p0gcnprq91e@example.com>
Subject: =?utf-8?B?QW4gZW1haWwgZ2VuZXJhdGVkIGluIGEgd29ya2Vy?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 7bit

Congratulations, you just sent an email from a worker.%
```

> Note: The path to the `.eml` file is different for each operating system.

### Receiving Email Locally

To test the receiving of an email by the email handler locally, you can use the following command:

```bash
pnpm dev
```

This will start the local development server and you can send emails to the `recipient@example.com` address.

Then, you can send an email to the `recipient@example.com` address using the following command:

```bash
curl --request POST 'http://localhost:5173/cdn-cgi/handler/email' \
  --url-query 'from=sender@example.com' \
  --url-query 'to=recipient@example.com' \
  --header 'Content-Type: application/json' \
  --data-raw 'Received: from smtp.example.com (127.0.0.1)
        by cloudflare-email.com (unknown) id 4fwwffRXOpyR
        for <recipient@example.com>; Tue, 27 Aug 2024 15:50:20 +0000
From: "John" <sender@example.com>
Reply-To: sender@example.com
To: recipient@example.com
Subject: Testing Email Workers Local Dev
Content-Type: text/html; charset="windows-1252"
X-Mailer: Curl
Date: Tue, 27 Aug 2024 08:49:44 -0700
Message-ID: <6114391943504294873000@ZSH-GHOSTTY>

Hi there'

```

You should see the content of the simulated email in the console output of the local development server.

```bash
{
  headers: [
    {
      key: 'received',
      value: 'from smtp.example.com (127.0.0.1) by cloudflare-email.com (unknown) id 4fwwffRXOpyR for <recipient@example.com>; Tue, 27 Aug 2024 15:50:20 +0000'
    },
    { key: 'from', value: '"John" <sender@example.com>' },
    { key: 'reply-to', value: 'sender@example.com' },
    { key: 'to', value: 'recipient@example.com' },
    { key: 'subject', value: 'Testing Email Workers Local Dev' },
    { key: 'content-type', value: 'text/html; charset="windows-1252"' },
    { key: 'x-mailer', value: 'Curl' },
    { key: 'date', value: 'Tue, 27 Aug 2024 08:49:44 -0700' },
    {
      key: 'message-id',
      value: '<6114391943504294873000@ZSH-GHOSTTY>'
    }
  ],
  from: { address: 'sender@example.com', name: 'John' },
  to: [ { address: 'recipient@example.com', name: '' } ],
  replyTo: [ { address: 'sender@example.com', name: '' } ],
  subject: 'Testing Email Workers Local Dev',
  messageId: '<6114391943504294873000@ZSH-GHOSTTY>',
  date: '2024-08-27T15:49:44.000Z',
  html: 'Hi there\n',
  attachments: []
}
```

## Production Deployment

To enable email handling in production, you need to have a Cloudflare zone with Email Routing enabled and at least one verified destination address.

You can refer to the following documentation:

- [Configure Email Routing Rules and Addresses](https://developers.cloudflare.com/email-routing/setup/email-routing-addresses/)
- [Enable Email Workers](https://developers.cloudflare.com/email-routing/email-workers/enable-email-workers/)
- [Send Email from Workers](https://developers.cloudflare.com/email-routing/email-workers/send-email-workers/)
- [Reply to Email from Workers](https://developers.cloudflare.com/email-routing/email-workers/reply-email-workers/)
- [Cloudflare Email Service Beta](https://blog.cloudflare.com/email-service/)
- [Sending email with Resend](/guides/email/sending-email)

## Further Reading

- [Cloudflare Email Routing Documentation](https://developers.cloudflare.com/email-routing/email-workers/)
- [Local Development for Email Workers](https://developers.cloudflare.com/email-routing/email-workers/local-development/)

## Future Improvements

- Demonstrate how to compose emails with [React Email](https://react.email).
